iT邦幫忙

2022 iThome 鐵人賽

DAY 6
0

華麗的特效很炫沒錯,但往往伴隨著效能卡頓的問題 QQ。在 網頁完整渲染的過程,會經過一系列的步驟,直到讓使用者看見畫面,光是改變元素大小,就會使流程在走一遍,這對瀏覽器是非常複雜的。

來源自 : https://gist.github.com/faressoft/36cdd64faae21ed22948b458e6bf04d5

有些 CSS 屬性改變會觸發一整個過程,尤其是改變物理上的距離、大小 (margin 、 width 等等),不過並不是所有的過程都必須經歷,在優化上可以盡量選擇影響幅度小的屬性,例如 : transform 就是一個好選擇。

舉例 文字畫線的動畫 就可以好幾種方法表示 :

這幾種牽涉網頁渲染路徑各有不同 ,會發現 widthright 多走了一個步驟

在 motion API 有提供 layer props ,把會使瀏覽器操作 layout 的部份進行優化,改成較少效能消耗的 transform,至於怎麼做呢 ? 初步了解是這樣的 :

  • 運用投射的概念,先算出目前元素跟最後動畫變更的中心點
  • 利用 transform 移動到正確中心點位置
  • 按照比例伸縮,但子元素也會被伸縮影響導致失真,伸縮前計算子元素的反伸縮比
  • 使用 scale 來縮放到最後動畫的大小

這裡只講到大概的原理,實際上應該會有產生正確位置的元素,並且在最後做交換。

有興趣可以參考 Matt Perry (framer motion 的維護者之一) 在 2020 Next.js conf 上 簡單解釋 layout props 做哪些事,以及他在 新的 motion library 上更深入的解釋

目錄

  1. 大風吹的改良版 : layout props
  2. 救救我阿我救我 : layout props 應用時機 ?
  3. 老花眼 : transform 的失真問題
  4. 阿嬤你怎麼沒感覺 ? transform 要用在區塊級元素

大風吹的改良版 : layout props

在前幾篇的文章中都使用 animateinitial 達到基本動畫操作,這些操作有一些共通點,他們不會 reflow ,像是 xy 位移是對應 transform ,基本上對效能來說負擔不大,因為都只走 paint 這條路。

官方範例 舉 flex 改變 justify-content 為例, 其屬性有 : flex-startcenterflex-end,那為什麼位移不能直接 flex-start 安排到 flex-end 呢 ? 這樣也省去 x 計算值的問題。

  • CSS
// 官方範例 : https://codesandbox.io/s/framer-motion-2-layout-animations-kij8p?from-embed

.switch {
  display: flex;
  justify-content: flex-start;
  //... 略
}
// 下面會看到 jsx 怎麼被加入 data-
.switch[data-isOn="true"] {
  justify-content: flex-end;
}
  • 結構
// 官方應用 data- 的方式改變,當 isOn state 改變就改變 justify-content 的值
<div className="switch" data-isOn={isOn} onClick={toggleSwitch}>
  {/* 添加了 layout */}
  <motion.div className="handle" layout transition={spring} />
</div>

先來看看 CSS trigger 對改變 justify-content 所觸發的渲染包含那些階段 :

從上圖可知道會影響到其他元素的位置,進行重排 (reflow) 的操作,來看一下實際上的差別。

  • 有 layout 沒 layout 區別 : 在 motion 元件裡操作 layout ,如果沒有 layout props 會發現明顯的截斷,有 layout props 動畫會保持絲滑的運行。

Magic !

(圖源自網路)

救救我阿我救我 : layout props 使用的時機 ?

官方列舉出使用 layout props 在哪些方面 :

  • 重新排序清單項目。跟 DOM 的節點新增/刪除有關
  • 改變本身的樣式,例如物理上的位置或大小 (position 、 width)
  • 在父元素改變內部排版,例如 : flexbox 或 grid,如上面的官方範例
  • 任何有關 layout 的屬性改變

老花眼 : transform 的失真問題

目前已知的問題是 box-shadowborder-radius 會在 transform 轉化過程中出現扭曲 (distort) 現象,為了解決這個問題,官方建議把他設為動畫值,motion 會自動矯正扭曲的問題,不過還是有一定的限制 :

  • border-radius 只能是用 px%
  • box-shadow : 只有單個 box-shadow 的情況。

如果不設成動畫值,就使用 還我漂漂拳,打回 style 裡面 :

<motion.div layout style={{ borderRadius: 20 }} />

可以看這篇 非常清楚的範例文章

  • 扭曲 QQ :
// CSS
.box {
  width: 20px;
  height: 20px;
  border-radius: 20px;  // 出現點比較早,因此在動畫操作發生扭曲
}

.box[data-expanded="true"] {
  width: 150px;
  height: 150px;
}
  
// JS
<motion.div
  layout
  className="box"
  data-expanded={expanded}
/>

明顯的漸變斷層

  • 矯正扭曲 :
// CSS
.box {
  width: 20px;
  height: 20px; 
}

.box[data-expanded="true"] {
  width: 150px;
  height: 150px;
}
  
// JS
<motion.div
  layout
  className="box"
  data-expanded={expanded}
  style={{
    borderRadius: '20px' // 還原在這裡使用
  }}
/>

變得更滑順多了

修正 : 第四天的 Pan 範例

我把 duration 調長,以至於可以看清楚轉化過程到底發生什麼問題 :

由於我使用 box-shadow 的 inset 做出月亮的缺口,導致會有個順間閃爍。
原始程式碼 :

<motion.div
    className="panBox"
    animate={{
        background: theme ? "#ABD9FF" : "#182747",
    }}
    >
    <motion.span
        className="panThumb"
        animate={{
            background: theme ? "#fa0" : "#182747",
            // 位移是使用 x 而不是 flex 的排版
            x: theme ? 0 : "calc(100px - 40px)",
            rotate: theme ? 0 : -160,
            // 罪魁禍首在這裡
            boxShadow: theme
                ? "inset 0px 0px rgb(0, 0, 0,0)"
                : "inset 15px 8px #fa0",
        }}
        transition={{
            duration: 1,
        }}
        onPan={(e, info) => {
            if (info.offset.x < 0) {
                setTheme(true);
            }
            if (info.offset.x > 0) {
                setTheme(false);
            }
        }}
    />
</motion.div>

接著補上我們的 layout 跟提到的修正方案 :

.panBox{
    // 改成 flex,不用再測距離
    display: flex;
    width : 100px;
    height: 40px;
    border: 1px solid #aaa;
    justify-content: flex-start;
    border-radius: 30px;
    box-shadow: 0 3px 5px rgba(0,0,0,.3),
}
// 當 theme 變化而觸發
.panBox[data-theme=false]{
    justify-content: flex-end;
}

.panThumb{
    //display: inline-block; 有 flex 之後就不用額外設定 inline-block 屬性
    width: 40px;
    height: 40px;
    cursor: pointer;  
    border-radius: 50%;
}

// JS
<motion.div
  className="panBox"
  data-theme={theme}
  layout // 有用到 flex 排版,使用 layout 補強
  animate={{
      background: theme ? "#ABD9FF" : "#182747",
  }}
  >
  <motion.span
      className="panThumb"
      layout
       animate={{
          background: theme ? "#fa0" : "#182747",
          rotate: theme ? 0 : -160,
          /* 改成動畫值 */
          boxShadow: theme
              ? "inset 15px 8px rgba(255, 170, 0,0)"
              : "inset 15px 8px rgba(255, 170, 0,1)",
      }}
      onPan={(e, info) => {
          if (info.offset.x < 0) {
              setTheme(true);
          }
          if (info.offset.x > 0) {
              setTheme(false);
          }
      }}
    />
</motion.div>

或者另一種還我漂漂拳,打回 style ,效果也是一樣的

style={{
    boxShadow: theme
            ? "inset 15px 8px rgba(255, 170, 0,0)"
            : "inset 15px 8px rgba(255, 170, 0,1)",
}}

一天又平安的滑過去了,感謝 layout 的努力

阿嬤你怎麼沒感覺 ? transform 要用在區塊級元素

初期常常寫出不知所以然的 CSS style,不知道什麼原因導致動畫不能動 QQ,曾經對著 span 操作 transform ,結果 span 一動也不動,這是因為 inline 屬性並沒有 transform 可以操作,像是 blockinline- 的屬性就可以。

參考 : CSS Transforms Module Level 1

A transformable element is an element in one of these categories:

  1. all elements whose layout is governed by the CSS box model except for non-replaced inline boxes, table-column boxes, and table-column-group boxes [CSS2],

總結

layout props 幫我們優化操作布局的元素,但也絕非萬能的,目前已存在的問題包含 :

  • 不行用在 Skew 歪斜屬性
  • border-radius 與 box-shadow 問題
  • 不支援 SVG 動畫

下一篇會延續 layout 剩下的部分,包含 LayoutGroup 與 layout 在不同地方上的應用。

參考資料

  1. 官方文件 : https://www.framer.com/docs/layout-animations/#troubleshooting
  2. 渲染歷程 : https://gist.github.com/faressoft/36cdd64faae21ed22948b458e6bf04d5
  3. 瀏覽器渲染知識 : Day08 X 瀏覽器架構演進史 & 渲染機制 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
  4. layout 範例 Everything about Framer Motion layout animations - Maxime Heckel's Blog
  5. 可以 transform 的元素 CSS Transforms Module Level 1

上一篇
#05 Kage Bunshin no Jutsu - Variants
下一篇
#07 Magic is happening again - layout type & layoutGroup
系列文
向網頁施點魔法粉 framer-motion 15
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言